home *** CD-ROM | disk | FTP | other *** search
/ GameStar 2004 April / Gamestar_61_2004-04_dvdb.iso / DVDStar / Editace / hltp.exe / {app} / Applications / QuArK / quarkpy / maphandles.py < prev    next >
Text File  |  2004-01-05  |  67KB  |  1,916 lines

  1. """   QuArK  -  Quake Army Knife
  2.  
  3. Map editor mouse handles.
  4. """
  5. #
  6. # Copyright (C) 1996-99 Armin Rigo
  7. # THIS FILE IS PROTECTED BY THE GNU GENERAL PUBLIC LICENCE
  8. # FOUND IN FILE "COPYING.TXT"
  9. #
  10.  
  11. #$Header: /cvsroot/quark/runtime/quarkpy/maphandles.py,v 1.37 2003/03/15 20:54:20 cdunde Exp $
  12.  
  13.  
  14.  
  15. #
  16. # This module manages the map "handles", i.e. the small active areas
  17. # that can be grabbed and dragged by the user on the map views.
  18. #
  19. # Generic handles are implemented in qhandles.py. This modules has
  20. # only the map-editor-specific handles.
  21. #
  22.  
  23.  
  24. import quarkx
  25. import math
  26. from qdictionnary import Strings
  27. import qhandles
  28. from maputils import *
  29. import mapentities
  30. import qmacro
  31.  
  32.  
  33.  
  34. #
  35. # The handle classes.
  36. #
  37.  
  38. class CenterHandle(qhandles.CenterHandle):
  39.     "Like qhandles.CenterHandle, but specifically for the map editor."
  40.     def menu(self, editor, view):
  41.         #
  42.         # FIXME: this is a pretty clunky way of making the
  43.         #  clicked on view available to entity menus, mebbe
  44.         #  should be cleaned up (view as 3rd parameter to entity
  45.         #  menus, perhaps?)
  46.         #
  47.         try:
  48.             editor.layout.clickedview = view
  49.         except:
  50.             editor.layout.clickedview = None
  51.         return mapentities.CallManager("menu", self.centerof, editor) + self.OriginItems(editor, view)
  52.  
  53. class IconHandle(qhandles.IconHandle):
  54.     "Like qhandles.IconHandle, but specifically for the map editor."
  55.     def menu(self, editor, view):
  56.         return mapentities.CallManager("menu", self.centerof, editor) + self.OriginItems(editor, view)
  57.  
  58.  
  59. def CenterEntityHandle(o, view, handleclass=IconHandle, pos=None):
  60.     if pos is None:
  61.         pos = o.origin
  62.     if pos is not None:
  63.         #
  64.         # Compute a handle for the entity angle.
  65.         #
  66.         h = []
  67.         #
  68.         for spec, cls in mapentities.ListAngleSpecs(o):
  69.             s = o[spec]
  70.             if s:
  71.                 stov, vtos = cls.map
  72.                 try:
  73.                     normal = stov(s)
  74.                 except:
  75.                     continue
  76.                 h = [cls(pos, normal, view.scale(), o, spec)]
  77.                 break
  78.         #
  79.         # Build a "circle" icon handle at the object origin.
  80.         #
  81.         new = handleclass(pos, o)   # the "circle" icon would be qhandles.mapicons[10], but it looks better with the entity icon itself
  82.         #
  83.         # Set the hint as the entity classname in blue ("?").
  84.         #
  85.         new.hint = "?" + o.shortname + "||This point represents an entity, i.e. an object that appears and interacts in the game when you play the map. The exact kind of entity depends on its 'classname' (its name).\n\nThis handle lets you move the entity with the mouse. Normally, the mouvement is done by steps of the size of the grid : if the entity was not aligned on the grid before the movement, it will not be after it. Hold down Ctrl to force the entity to the grid."
  86.         #
  87.         # Return the handle
  88.         #
  89.         return h+[new]
  90.  
  91.     else:
  92.         #
  93.         # No "origin".
  94.         #
  95.         return []
  96.  
  97.  
  98.  
  99. class FaceHandleCursor:
  100.     "Special class to compute the mouse cursor shape based on the visual direction of a face."
  101.  
  102.     def getcursor(self, view):
  103.         n = view.proj(self.pos + self.face.normal) - view.proj(self.pos)
  104.         dx, dy = abs(n.x), abs(n.y)
  105.         if dx*2<=dy:
  106.             if (dx==0) and (dy==0):
  107.                 return CR_ARROW
  108.             else:
  109.                 return CR_SIZENS
  110.         elif dy*2<=dx:
  111.             return CR_SIZEWE
  112.         elif (n.x>0)^(n.y>0):
  113.             return CR_SIZENESW
  114.         else:
  115.             return CR_SIZENWSE
  116.  
  117.  
  118. def completeredimage(face, new):
  119.     #
  120.     # Complete a red image with the whole polyhedron.
  121.     # (red images cannot be reduced to a single face; even if
  122.     #  we drag just a face, we want to see the whole polyhedron)
  123.     #
  124.     gr = []
  125.     for src in face.faceof:
  126.         if src.type == ":p":
  127.             poly = quarkx.newobj("redimage:p")
  128.             t = src
  129.             while t is not None:
  130.                 for q in t.subitems:
  131.                     if (q.type==":f") and not (q is face):
  132.                          poly.appenditem(q.copy())
  133.                 t = t.treeparent
  134.             poly.appenditem(new.copy())
  135.             gr.append(poly)
  136.     if len(gr):
  137.         return gr
  138.     else:
  139.         return [new]
  140.  
  141.  
  142.  
  143. class FaceHandle(qhandles.GenericHandle):
  144.     "Center of a face."
  145.  
  146.     undomsg = Strings[516]
  147.     hint = "move this face (Ctrl key: force center to grid)||This handle lets you scroll this face, thus distort the polyhedron(s) that contain it.\n\nNormally, the face can be moved by steps of the size of the grid; holding down the Ctrl key will force the face center to be exactly on the grid."
  148.  
  149.     def __init__(self, pos, face):
  150.         qhandles.GenericHandle.__init__(self, pos)
  151.         self.face = face
  152.         cur = FaceHandleCursor()
  153.         cur.pos = pos
  154.         cur.face = face
  155.         self.cursor = cur.getcursor
  156.  
  157.     def menu(self, editor, view):
  158.         self.click(editor)
  159.         return mapentities.CallManager("menu", self.face, editor) + self.OriginItems(editor, view)
  160.  
  161.     def drag(self, v1, v2, flags, view):
  162.         delta = v2-v1
  163.         g1 = 1
  164.         if flags&MB_CTRL:
  165.             pos0 = self.face.origin
  166.             if pos0 is not None:
  167.                 pos1 = qhandles.aligntogrid(pos0+delta, 1)
  168.                 delta = pos1 - pos0
  169.                 g1 = 0
  170.         if g1:
  171.             delta = qhandles.aligntogrid(delta, 0)
  172.         self.draghint = vtohint(delta)
  173.         if delta or (flags&MB_REDIMAGE):
  174.             new = self.face.copy()
  175.             if self.face.faceof[0].type == ":p":
  176.                 delta = self.face.normal * (self.face.normal*delta)  # projection of 'delta' on the 'normal' line
  177.             new.translate(delta)
  178.             if flags&MB_DRAGGING:    # the red image contains the whole polyhedron(s), not the single face
  179.                 new = completeredimage(self.face, new)
  180.             else:
  181.                 new = [new]
  182.         else:
  183.             new = None
  184.         return [self.face], new
  185.  
  186.     def leave(self, editor):
  187.         src = self.face.faceof
  188.         if (len(src)==1) and (src[0].type == ":p"):
  189.             editor.layout.explorer.uniquesel = src[0]
  190.  
  191.  
  192. class PFaceHandle(FaceHandle):
  193.     "Center of a face, but unselected (as part of a selected poly)."
  194.  
  195.     def draw(self, view, cv, draghandle=None):
  196.         p = view.proj(self.pos)
  197.         if p.visible:
  198.             cv.reset()
  199.             cv.brushcolor = view.darkcolor
  200.             cv.rectangle(p.x-3, p.y-3, p.x+4, p.y+4)
  201.  
  202.     def click(self, editor):
  203.         editor.layout.explorer.uniquesel = self.face
  204.         return "S"
  205.  
  206.     def leave(self, editor):
  207.         pass
  208.  
  209.  
  210.  
  211. class MapRotateHandle(qhandles.Rotate3DHandle):
  212.     "Like Rotate3DHandle, but specifically for the map editor."
  213.  
  214.     MODE = SS_MAP
  215.  
  216.  
  217.  
  218. class FaceNormalHandle(MapRotateHandle):
  219.     "3D rotating handle, for faces."
  220.  
  221.     undomsg = Strings[517]
  222.     hint = "rotate this face (Ctrl key: force to a common angle)||This handle lets you rotate the face around its center. Use it to distort the polyhedron(s).\n\nYou can set any angle unless you hold down the Ctrl key; in this case, you can only set 'round' angle values. See the Configuration dialog box, Map, Display."
  223.  
  224.     def __init__(self, center, vtx, face, scale1):
  225.         MapRotateHandle.__init__(self, center, face.normal, scale1, qhandles.mapicons[11])
  226.         self.face = face
  227.         self.vtx = vtx
  228.  
  229.     def menu(self, editor, view):
  230.         return qmenu.catmenus(MapRotateHandle.menu(self, editor, view),
  231.           mapentities.CallManager("menu", self.face, editor))
  232.  
  233.     def draw(self, view, cv, draghandle=None):
  234.         cv.reset()
  235.         p1, p2 = view.proj(self.center), view.proj(self.pos)
  236.         fromback = view.vector(self.center)*self.face.normal < 0
  237.         if fromback:
  238.             self.draw1(view, cv, p1, p2, 1)
  239.             if p1.visible:
  240.                 cv.rectangle(p1.x-3, p1.y-3, p1.x+4, p1.y+4)
  241.         else:
  242.             oldpc = cv.pencolor
  243.         cv.pencolor = YELLOW
  244.         for v in self.vtx:
  245.             p = view.proj(v)
  246.             cv.line(p, p1)
  247.         if not fromback:
  248.             cv.pencolor = oldpc
  249.             if p1.visible:
  250.                 cv.rectangle(p1.x-3, p1.y-3, p1.x+4, p1.y+4)
  251.             self.draw1(view, cv, p1, p2, 0)
  252.         view.drawmap(self.face, 0)
  253.  
  254.     def dragop(self, flags, av):
  255.         new = None
  256.         if av is not None:
  257.             new = self.face.copy()
  258.             new.distortion(av, self.center)
  259.             if flags&MB_DRAGGING:    # the red image contains the whole polyhedron(s), not the single face
  260.                 new = completeredimage(self.face, new)
  261.             else:
  262.                 new = [new]
  263.         return [self.face], new, av
  264.  
  265.  
  266.  
  267. class Angles3DHandle(MapRotateHandle):
  268.     "3D rotating handle, for 'angles'-like Specifics."
  269.  
  270.     hint = "entity pointing angle (any 3D direction)||Lets you set the direction the entity is 'looking' at. Hold down Ctrl to force the angle to some 'round' value."
  271.     map = (qhandles.angles2vec, qhandles.vec2angles)
  272.     ii = 12
  273.  
  274.     def __init__(self, pos, normal, scale1, entity, spec):
  275.         MapRotateHandle.__init__(self, pos, normal, scale1, qhandles.mapicons[self.ii])
  276.         self.entity = entity
  277.         self.spec = spec
  278.  
  279.     def menu(self, editor, view):
  280.         return qmenu.catmenus(MapRotateHandle.menu(self, editor, view),
  281.           mapentities.CallManager("menu", self.entity, editor))
  282.  
  283.     def dragop(self, flags, av):
  284.         new = None
  285.         if av is not None:
  286.             stov, vtos = self.map
  287.             new = self.entity.copy()
  288.             s = vtos(av, new[self.spec])
  289.             av = stov(s)
  290.             new[self.spec] = s
  291.             new = [new]
  292.         return [self.entity], new, av
  293.  
  294.  
  295. class Angle2DHandle(Angles3DHandle):
  296.     "2D rotating handle, for 'angle'-like Specifics."
  297.  
  298.     hint = "entity pointing angle (2D direction only, or up or down)||Lets you set the direction the entity is 'looking' at. This has various meaning for various entities : for most ones, it is the direction it is facing when the map starts; for buttons or doors, it is the direction the button or door moves.\n\nYou can set any horizontal angle, as well as 'up' and 'down'. Hold down Ctrl to force the angle to a 'round' value."
  299.     map = (qhandles.angle2vec, qhandles.vec2angle)
  300.     ii = 13
  301.  
  302.  
  303.  
  304. class PolyHandle(CenterHandle):
  305.     "Center of polyhedron Handle."
  306.  
  307.     undomsg = Strings[515]
  308.     hint = "move polyhedron (Ctrl key: force center to grid)||Lets you move this polyhedron.\n\nYou can move it by steps equal to the grid size. This means that if it is not on the grid now, it will not be after the move, either. You can force it on the grid by holding down the Ctrl key, but be aware that this forces its center to the grid, not all its faces. For cubic polyhedron, you may need to divide the grid size by two before you get the expected results."
  309.  
  310.     def __init__(self, pos, centerof):
  311.         CenterHandle.__init__(self, pos, centerof, 0x202020, 1)
  312.  
  313.     def click(self, editor):
  314.         if not self.centerof.selected:   # case of the polyhedron center handle if only a face is selected
  315.             editor.layout.explorer.uniquesel = self.centerof
  316.             return "S"
  317.  
  318.  
  319.  
  320. #
  321. # if vtxes contains a point close to the line thru v along axis, return
  322. #  it, otherwise v+axis
  323. #
  324. def getotherfixed(v, vtxes, axis):
  325.     for v2 in vtxes:
  326.         if not (v-v2):
  327.             continue;
  328.         perp = perptonormthru(v2,v,axis.normalized)
  329.         if abs(perp)<.05:
  330.             return v2
  331.     return v+axis
  332.  
  333.  
  334. #
  335. # A handle for edges
  336. #
  337. class EdgeHandle(qhandles.GenericHandle):
  338.     undomsg = "drag edge"
  339.     hint = "tag this edge for lining up objects, etc."
  340.  
  341.     def __init__(self, face, vtx1, vtx2):
  342.         pos = (vtx2+vtx1)/2
  343.         qhandles.GenericHandle.__init__(self, pos)
  344.         self.face = face
  345.         self.vtx1, self.vtx2 = vtx1, vtx2
  346.         cur = FaceHandleCursor()
  347.         cur.pos = pos
  348.         cur.face = face
  349.         self.cursor = cur.getcursor
  350.  
  351.     def menu(self, editor, view):
  352.         self.click(editor)
  353.         editor.layout.clickedview = view
  354.         return self.OriginItems(editor, view)
  355.  
  356.     def draw(self, view, cv, draghandle=None):
  357.         p = view.proj(self.pos)
  358.         oldcolor = cv.pencolor
  359.         p1 = view.proj(self.vtx1)
  360.         p2 = view.proj(self.vtx2)
  361.         cv.pencolor = oldcolor
  362.         if p.visible:
  363.             cv.reset()
  364.             cv.brushcolor = view.darkcolor
  365.             radius = 3
  366.             cv.ellipse(p.x-radius, p.y-radius, p.x+radius+1, p.y+radius+1)
  367. #            cv.rectangle(p.x-3, p.y-3, p.x+4, p.y+4)
  368.  
  369.  
  370.  
  371. class VertexHandle(qhandles.GenericHandle):
  372.     "A polyhedron vertex."
  373.  
  374.     undomsg = Strings[525]
  375.     hint = "move vertex and distort polyhedron (Alt key: restricted to one face only)||By dragging this point, you can distort the polyhedron in a way that looks like you are moving the vertex of the polyhedron.\n\nBe aware that you might not always get the expected results, because you are not really dragging the vertex, but just rotating the adjacent faces in a way that simulates the vertex movement. If you move the vertex too far away, it might just disappear. Polyhedrons are always convex, so that you cannot do just anything you like with them.\n\nHolding down the Alt key to let only one face move. Holding down Ctrl will force the vertex to the grid."
  376.  
  377.     def __init__(self, pos, poly):
  378.         qhandles.GenericHandle.__init__(self, pos)
  379.         self.poly = poly
  380.         self.cursor = CR_CROSSH
  381.  
  382.     def menu(self, editor, view):
  383.  
  384.         def forcegrid1click(m, self=self, editor=editor, view=view):
  385.             self.Action(editor, self.pos, self.pos, MB_CTRL, view, Strings[560])
  386.  
  387.         def cutcorner1click(m, self=self, editor=editor, view=view):
  388.             #
  389.             # Find all edges and faces issuing from the given vertex.
  390.             #
  391.             edgeends = []
  392.             faces = []
  393.             for f in self.poly.faces:
  394.                 vertices = f.verticesof(self.poly)
  395.                 for i in range(len(vertices)):
  396.                     if not (vertices[i]-self.pos):
  397.                         edgeends.append(vertices[i-1])
  398.                         edgeends.append(vertices[i+1-len(vertices)])
  399.                         if not (f in faces):
  400.                             faces.append(f)
  401.             #
  402.             # Remove duplicates.
  403.             #
  404.             edgeends1 = []
  405.             for i in range(len(edgeends)):
  406.                 e1 = edgeends[i]
  407.                 for e2 in edgeends[:i]:
  408.                     if not (e1-e2):
  409.                         break
  410.                 else:
  411.                     edgeends1.append(e1)
  412.             #
  413.             # Compute the mean point of edgeends1.
  414.             # The new face will go through the point in the middle between this and the vertex.
  415.             #
  416.             pt = reduce(lambda x,y: x+y, edgeends1)/len(edgeends1)
  417.             #
  418.             # Compute the mean normal vector from the adjacent faces' normal vector.
  419.             #
  420.             n = reduce(lambda x,y: x+y, map(lambda f: f.normal, faces))
  421.             #
  422.             # Force "n" to be perpendicular to the screen direction.
  423.             #
  424.             vertical = view.vector(self.pos).normalized   # vertical vector at this point
  425.             n = (n - vertical * (n*vertical)).normalized
  426.             #
  427.             # Find a "model" face for the new one.
  428.             #
  429.             bestface = faces[0]
  430.             for f in faces[1:]:
  431.                 if abs(f.normal*vertical) < abs(bestface.normal*vertical):
  432.                     bestface = f
  433.             #
  434.             # Build the new face.
  435.             #
  436.             newface = bestface.copy()
  437.             newface.shortname = "corner"
  438.             newface.distortion(n, self.pos)
  439.             #
  440.             # Move the face to its correct position.
  441.             #
  442.             delta = 0.5*(pt-self.pos)
  443.             delta = n * (delta*n)
  444.             newface.translate(delta)
  445.             #
  446.             # Insert the new face into the polyhedron.
  447.             #
  448.             undo = quarkx.action()
  449.             undo.put(self.poly, newface)
  450.             editor.ok(undo, Strings[563])
  451.  
  452.         return [qmenu.item("&Cut out corner", cutcorner1click, "|This command cuts out the corner of the polyhedron. It does so by adding a new face near the vertex you right-clicked on. The new face is always perpendicular to the view."),
  453.                 qmenu.sep,
  454.                 qmenu.item("&Force to grid", forcegrid1click,
  455.                   "force vertex to grid")] + self.OriginItems(editor, view)
  456.  
  457.  
  458.     def draw(self, view, cv, draghandle=None):
  459.         p = view.proj(self.pos)
  460.         if p.visible:
  461.             cv.reset()
  462.             cv.brushcolor = view.color
  463.             cv.rectangle(p.x-0.501, p.y-0.501, p.x+2.499, p.y+2.499)
  464.  
  465.  
  466.     def ok2(self, editor,undo,old,new):
  467.         qhandles.GenericHandle.ok(self,editor,undo,old,new)
  468.         for poly in new:
  469.             for face in poly.faces:
  470.                 face.enhrevert()
  471.         editor.layout.explorer.sellist=new
  472.  
  473.     def drag(self, v1, v2, flags, view):
  474.  
  475.         #### Vertex Dragging Code by Tim Smith ####
  476.  
  477.         #
  478.         # compute the projection of the starting point? onto the
  479.         # screen.
  480.         #
  481.         p0 = view.proj(self.pos)
  482.         if not p0.visible: return
  483.  
  484.         #
  485.         # save a copy of the original faces
  486.         #
  487.         orgfaces = self.poly.subitems
  488.  
  489.         #
  490.         # first, loop through the faces to see if we are draging
  491.         # more than one point at a time.  This loop uses the distance
  492.         # between the projected screen position of the starting point
  493.         # and the project screen position of the vertex.
  494.         #
  495.         dragtwo = 0
  496.         for f in self.poly.faces:
  497.             if f in orgfaces:
  498.                 if abs(self.pos*f.normal-f.dist) < epsilon:
  499.                     foundcount = 0
  500.                     for v in f.verticesof(self.poly):
  501.                         p1 = view.proj(v)
  502.                         if p1.visible:
  503.                             dx, dy = p1.x-p0.x, p1.y-p0.y
  504.                             d = dx*dx + dy*dy
  505.                             if d < epsilon:
  506.                                 foundcount = foundcount + 1
  507.                     if foundcount == 2:
  508.                         dragtwo = 1
  509.  
  510.         #
  511.         # if the ALT key is pressed
  512.         #
  513.         if (flags&MB_ALT) != 0:
  514.  
  515.             #
  516.             # loop through the list of points looking for the edge
  517.             # that is closest to the new position.
  518.             #
  519.             # WARNING - THIS CODE ASSUMES THAT THE VERTECIES ARE ORDERED.
  520.             #   IT ASSUMES THAT V1->V2 MAKE AND EDGE, V2->V3 etc...
  521.             #
  522.             # Note by Armin: this assumption is correct.
  523.             #
  524.             delta = v2 - v1
  525.             mindist = 99999999
  526.             dv1 = self.pos + delta
  527.             xface = -1
  528.             xvert = -1
  529.             for f in self.poly.faces:
  530.                 xface = xface + 1
  531.                 if f in orgfaces:
  532.                     if abs(self.pos*f.normal-f.dist) < epsilon:
  533.                         vl = f.verticesof (self.poly)
  534.                         i = 0
  535.                         while i < len (vl):
  536.                             v = vl [i]
  537.                             p1 = view.proj(v)
  538.                             if p1.visible:
  539.                                 dx, dy = p1.x-p0.x, p1.y-p0.y
  540.                                 d = dx*dx + dy*dy
  541.                                 if d < epsilon:
  542.                                     dv2 = v - vl [i - 1]
  543.                                     if dv2:
  544.                                         cp = (v - dv1) ^ dv2
  545.                                         num = (cp.x * cp.x + cp.y * cp.y + cp.z * cp.z)
  546.                                         den = (dv2.x * dv2.x + dv2.y * dv2.y + dv2.z * dv2.z)
  547.                                         if num / den < mindist:
  548.                                             mindist = num / den
  549.                                             vtu1 = v
  550.                                             vtu2 = vl [i - 1]
  551.                                             xvert = i - 1
  552.                                     dv2 = v - vl [i + 1 - len (vl)]
  553.                                     if dv2:
  554.                                         cp = (v - dv1) ^ dv2
  555.                                         num = (cp.x * cp.x + cp.y * cp.y + cp.z * cp.z)
  556.                                         den = (dv2.x * dv2.x + dv2.y * dv2.y + dv2.z * dv2.z)
  557.                                         if num / den < mindist:
  558.                                             mindist = num / den
  559.                                             vtu1 = v
  560.                                             vtu2 = vl [i + 1 - len (vl)]
  561.                                             xvert = i
  562.                             i = i + 1
  563.  
  564.             #
  565.             # If a edge was found
  566.             #
  567.             if mindist < 99999999:
  568.                 #
  569.                 # Compute the orthogonal projection of the destination point onto the
  570.                 # edge.  Use the projection to compute a new value for delta.
  571.                 #
  572.                 temp = dv1 - vtu1
  573.                 if not temp:
  574.                     vtu1, vtu2 = vtu2, vtu1
  575.                 temp = dv1 - vtu1
  576.                 vtu2 = vtu2 - vtu1
  577.                 k = (temp * vtu2) / (abs (vtu2) * abs (vtu2))
  578.                 projdv1 = k * vtu2
  579.                 temp = projdv1 + vtu1
  580.  
  581.                 #
  582.                 # Compute the final value for the delta
  583.                 #
  584.                 if flags&MB_CTRL:
  585.                     delta = qhandles .aligntogrid (temp, 1) - self .pos
  586.                 else:
  587.                     delta = qhandles .aligntogrid (temp - self .pos, 0)
  588.  
  589.         #
  590.         # Otherwise
  591.         #
  592.         else:
  593.             #
  594.             # if the control key is pressed, align the destination point to grid
  595.             #
  596.             if flags&MB_CTRL:
  597.                 v2 = qhandles.aligntogrid(v2, 1)
  598.  
  599.             #
  600.             # compute the change in position
  601.             #
  602.             delta = v2-v1
  603.  
  604.             #
  605.             # if the control is not pressed, align delta to the grid
  606.             #
  607.             if not (flags&MB_CTRL):
  608.                 delta = qhandles.aligntogrid(delta, 0)
  609.  
  610.         #
  611.         # if we are dragging
  612.         #
  613.         self.draghint = vtohint(delta)
  614.         if delta or (flags&MB_REDIMAGE):
  615.  
  616.             #
  617.             # make a copy of the polygon being drug
  618.             #
  619.             new = self.poly.copy()
  620.  
  621.             #
  622.             # loop through the faces
  623.             #
  624.             for f in self.poly.faces:
  625.  
  626.                 #
  627.                 # if this face is part of the original group
  628.                 #
  629.                 if f in orgfaces:
  630.                     #
  631.                     # if the point is on the face
  632.                     #
  633.                     if abs(self.pos*f.normal-f.dist) < epsilon:
  634.  
  635.                         #
  636.                         # collect a list of verticies on the face along
  637.                         # with the distances from the destination point.
  638.                         # also, count the number of vertices.  NOTE:
  639.                         # this loop uses the actual distance between the
  640.                         # two points and not the screen distance.
  641.                         #
  642.                         foundcount = 0
  643.                         vlist = []
  644.                         mvlist = []
  645.                         for v in f.verticesof(self.poly):
  646.                             p1 = view.proj(v)
  647.                             if p1.visible:
  648.                                 dx, dy = p1.x-p0.x, p1.y-p0.y
  649.                                 d = dx*dx + dy*dy
  650.                             else:
  651.                                 d = 1
  652.                             if d < epsilon:
  653.                                 foundcount = foundcount + 1
  654.                                 mvlist .append (v)
  655.                             else:
  656.                                 d = v - self .pos
  657.                                 vlist.append((abs (d), v))
  658.  
  659.                         #
  660.                         # sort the list of vertecies, this places the
  661.                         # most distant point at the end
  662.                         #
  663.                         vlist.sort ()
  664.                         vmax = vlist [-1][1]
  665.  
  666.                         #
  667.                         # if we are draging two vertecies
  668.                         #
  669.                         if dragtwo:
  670.  
  671.                             #
  672.                             # if this face does not have more than one vertex
  673.                             # selected, then skip
  674.                             #
  675.                             if foundcount != 2:
  676.                                 continue
  677.  
  678.                             #
  679.                             # the rotational axis is between the two
  680.                             # points being drug.  the reference point is
  681.                             # the most distant point
  682.                             #
  683.                             rotationaxis = mvlist [0] - mvlist [1]
  684.                             otherfixed =getotherfixed(vmax, mvlist, rotationaxis)
  685.                             fixedpoints = vmax, otherfixed
  686.  
  687.                         #
  688.                         # otherwise, we are draging one
  689.                         #
  690.                         else:
  691.  
  692.                             #
  693.                             # if this face does not have any of the selected
  694.                             # vertecies, then skip
  695.                             #
  696.                             if foundcount == 0:
  697.                                 continue
  698.  
  699.                             #
  700.                             # sort the vertex list and use the last vertex as
  701.                             # a rotational reference point
  702.                             # (already done, seems to me)
  703.                         #    vlist.sort()
  704.                         #    vmax = vlist[-1][1]
  705.  
  706.  
  707.                             #
  708.                             # METHOD A: Using the two most distant points
  709.                             # as the axis of rotation
  710.                             #
  711.                             if not (flags&MB_SHIFT):
  712.                                 rotationaxis = (vmax - vlist [-2] [1])
  713.                                 fixedpoints = vmax, vlist[-2][1]
  714.  
  715.                             #
  716.                             # METHOD B: Using the most distant point, rotate
  717.                             # along the perpendicular to the vector between
  718.                             # the most distant point and the position
  719.                             #
  720.                             else:
  721.                                 rotationaxis = (vmax - self .pos) ^ f .normal
  722.                                 otherfixed =getotherfixed(vmax, vlist, rotationaxis)
  723.                                 fixedpoints = vmax, otherfixed
  724.  
  725.                         #
  726.                         # apply the rotation axis to the face (requires that
  727.                         # rotationaxis and vmax to be set)
  728.                         #
  729.                         newpoint = self.pos+delta
  730.                         nf = new.subitem(orgfaces.index(f))
  731.  
  732.                         def pointsok(new,fixed):
  733.                             #
  734.                             # coincident not OK
  735.                             #
  736.                             if not new-fixed[0]: return 0
  737.                             if not new-fixed[1]: return 0
  738.                             #
  739.                             # colinear also not OK
  740.                             #
  741.                             if abs((new-fixed[0]).normalized*(new-fixed[1]).normalized)>.999999:
  742.                                return 0
  743.                             return 1
  744.  
  745.                         if pointsok(newpoint,fixedpoints):
  746.                             tp = nf.threepoints(2)
  747.                             x,y = nf.axisbase()
  748.                             def proj1(p, x=x,y=y,v=vmax):
  749.                                 return (p-v)*x, (p-v)*y
  750.                             tp = tuple(map(proj1, tp))
  751.                             nf.setthreepoints((newpoint,fixedpoints[0],fixedpoints[1]),0)
  752.  
  753.                             newnormal = rotationaxis ^ (self.pos+delta-vmax)
  754.                             testnormal = rotationaxis ^ (self.pos-vmax)
  755.                             if newnormal:
  756.                                 if testnormal * f.normal < 0.0:
  757.                                     newnormal = -newnormal
  758.  
  759.  
  760.                             if nf.normal*newnormal<0.0:
  761.                                 nf.swapsides()
  762.                             x,y=nf.axisbase()
  763.                             def proj2(p,x=x,y=y,v=vmax):
  764.                                 return v+p[0]*x+p[1]*y
  765.                             tp = tuple(map(proj2,tp))
  766.                             #
  767.                             # Code 4 for NuTex
  768.                             #
  769.                             nf.setthreepoints(tp ,2)
  770.  
  771.  
  772.  
  773.                 # if the face is not part of the original group
  774.                 #
  775.  
  776.                 else:
  777.                     if not (flags&MB_DRAGGING):
  778.                         continue   # face is outside the polyhedron
  779.                     nf = f.copy()   # put a copy of the face for the red image only
  780.                     new.appenditem(nf)
  781.  
  782.         #
  783.         # final code
  784.         #
  785.             new = [new]
  786.         else:
  787.             new = None
  788.         return [self.poly], new
  789.  
  790.  
  791.  
  792. class MapEyeDirection(qhandles.EyeDirection):
  793.  
  794.     MODE = SS_MAP
  795.  
  796.  
  797. class PropGlueDlg (SimpleCancelDlgBox):
  798.  
  799.     #
  800.     # dialog layout
  801.     #
  802.  
  803.     size = (130, 75)
  804.     dfsep = 0.6       # separation at 40% between labels and edit boxes
  805.     dlgflags = FWF_KEEPFOCUS
  806.  
  807.     dlgdef = """
  808.         {
  809.         Style = "9"
  810.         Caption = "Glue Proportion"
  811.  
  812.         prop: =
  813.         {
  814.         Txt = "Proportion:"
  815.         Typ = "EF001"
  816.         Hint = "L-end will be moved to this proportion of the distance" $0D "from the texture origin to the tagged point"
  817.         }
  818.         cancel:py = {Txt="" }
  819.     }
  820.     """
  821.  
  822.     #
  823.     # __init__ initialize the object
  824.     #
  825.  
  826.     def __init__(self, form, action):
  827.  
  828.     #
  829.     # General initialization of some local values
  830.     #
  831.  
  832.         src = quarkx.newobj(":")
  833.         initialvalue = quarkx.setupsubset(SS_MAP, "Options")["PropTexGlue"]
  834.         if initialvalue is None:
  835.             initialvalue = 1,
  836.         src["prop"]=initialvalue
  837.         self.action=action
  838.         SimpleCancelDlgBox.__init__(self, form, src)
  839.  
  840.     #
  841.     # This is executed when the data changes, close when a new
  842.     #   name is provided
  843.     #
  844.     def datachange(self, df):
  845.         quarkx.setupsubset(SS_MAP, "Options")["PropTexGlue"] = self.src["prop"]
  846.         self.close()
  847.  
  848.  
  849.     #
  850.     # This is executed when the OK button is pressed
  851.     #   FIXME: 'local' code doesn't work right, dialog
  852.     #   would need some redesign
  853.     #
  854.     def ok(self):
  855.         prop, = self.src["prop"]
  856.         self.prop = prop
  857.         self.action(self)
  858.  
  859.  
  860. class CyanLHandle(qhandles.GenericHandle):
  861.     "Texture moving of faces : cyan L vertices."
  862.  
  863.     def __init__(self, n, tp4, face, texsrc):
  864.         self.tp4 = tp4
  865.         self.pos = tp4[n]
  866.         self.n = n
  867.         self.face = face
  868.         self.cursor = (CR_DRAG, CR_LINEARV, CR_LINEARV, CR_CROSSH)[n]
  869.         self.texsrc = texsrc
  870.         self.undomsg = Strings[(598,617,617,618)[n]]
  871.         self.hint = ("offset texture on face", "enlarge or distort 1st texture axis",
  872.          "enlarge or distort 2nd texture axis", "rotate texture")[n] +   \
  873.          "||Use the 4 handles at the corners of this 'L' to scroll or rotate the texture on the face.\n\nThe center of the 'L' lets you scroll the texture; the two ends lets you enlarge and distort the texture in the corresponding directions; the 4th point lets you rotate the texture."
  874.  
  875.     def menu(self,editor,view):
  876.         import plugins.tagging
  877.  
  878.         norm=self.face.normal
  879.         facepoint=self.face.dist*norm
  880.  
  881.         #
  882.         # .axisbase() method recovers orthogonal vectors
  883.         # in plane of face, y horizontal.
  884.         #
  885.         x, y = self.face.axisbase()
  886.  
  887.         #
  888.         # converts p to coordinates with origin org,
  889.         #  axisbase axis vectors.  See Python Tute
  890.         #  4.7.1. for the default argument constructioin
  891.         #  x=x, etc.  Here the x on the left represents
  892.         #  x in the function, x on the right x outside;
  893.         #  when the function is called, we only need to
  894.         #  specify the first two arguments and the other
  895.         #  two will take their default values as provided
  896.         #  in the lines above.
  897.         #
  898.         def toAxisBase(p,org,x=x,y=y,z=norm):
  899.             diff = p-org
  900.             return quarkx.vect(diff*x, diff*y, diff*z)
  901.  
  902.         #
  903.         # Also projects onto plane
  904.         #
  905.         def fromAxisBase(p, org, x=x, y=y):
  906.             return org+p.x*x+p.y*y
  907.  
  908.         def onFace(p,norm=norm, facepoint=facepoint):
  909.             if norm*(facepoint-p)<.0001:
  910.                 return 1
  911.             else:
  912.                 return 0
  913.  
  914.         def toFace(p,norm=norm, facepoint=facepoint):
  915.             return projectpointtoplane(p,norm,facepoint,norm)
  916.  
  917.         #
  918.         # Glue to tagged point
  919.         #
  920.         tagged=plugins.tagging.gettaggedpt(editor)
  921.         if tagged is not None:
  922.             taggedonface = onFace(tagged)
  923.         else:
  924.             taggedonface=0
  925.  
  926.         def glueClick(m,tagged=tagged,self=self,editor=editor,toFace=toFace):
  927.             #
  928.             # We only want to glue to points on the plane of theface, so
  929.             #  any off-plane points are projected to the plane.
  930.             #
  931.             tagged=toFace(tagged)
  932.             #
  933.             # These are the locations of the four Cyan handles
  934.             #  (origin, s, t, rotation)
  935.             #
  936.             p1, p2, p3, p4 = self.tp4
  937.             #
  938.             # We'll operate on a copy, then undo.exchange()
  939.             #
  940.             newface = self.face.copy()
  941.             #
  942.             # The n-value of the handle says which of
  943.             #   the four handles this one is, starting # 0.
  944.             # Below assumes that menuitem is disabled for n=3
  945.             #
  946.             if self.n==0:
  947.                 diff = p1-tagged
  948.                 #
  949.                 # setthreepoints(texp,2) for setting a texture scale.
  950.                 #
  951.                 newface.setthreepoints((tagged,p2-diff,p3-diff),2)
  952.             elif self.n==1:
  953.                 newface.setthreepoints((p1,tagged,p3),2)
  954.             elif self.n==2:
  955.                 newface.setthreepoints((p1,p2,tagged),2)
  956.             #
  957.             # And now the undo - exchange sequence.
  958.             #
  959.             undo=quarkx.action()
  960.             undo.exchange(self.face,newface)
  961.             editor.ok(undo,'Glue to Tagged')
  962.  
  963.  
  964.         glueitem = qmenu.item('Glue to tagged',glueClick)
  965.         if not taggedonface or self.n==3:
  966.             glueitem.state=qmenu.disabled
  967.  
  968.         #
  969.         # Align to tagged edge (by rotation, to the one
  970.         #   that's closest to being aligned)
  971.         #
  972.         edge = gettaggededge(editor)
  973.  
  974.         def alignClick(m,self=self,edge=edge,editor=editor,
  975.                        toAxisBase=toAxisBase,fromAxisBase=fromAxisBase):
  976.             p1, p2, p3, p4 = self.tp4
  977.             def proj1(p,org=p1,toAxisBase=toAxisBase):
  978.                 return toAxisBase(p,org)
  979.             p2, p3 = proj1(p2), proj1(p3)
  980.             edge = toAxisBase(edge[1], edge[0])
  981.             def toAngle(p):
  982.                 p = p.normalized
  983.                 return math.atan2(p.y, p.x)
  984.             #
  985.             # 'map' applies the function to the list/tuple.
  986.             #  in Minipy, result is a list and needs to
  987.             #  be coerced to tuple.
  988.             #
  989.             edgeang, p2ang, p3ang = tuple(map(toAngle, (edge, p2, p3)))
  990.             #
  991.             # Find the smallest angle that will rotate the threepoints
  992.             #  into alignment
  993.             #
  994.             rotang=2*math.pi
  995.             for edgeang0 in edgeang, -edgeang:
  996.                 for p2ang0 in p2ang, p3ang:
  997.                     diffang = edgeang0-p2ang0
  998.                     if abs(rotang)>abs(diffang):
  999.                         rotang=diffang
  1000.             rotmat = matrix_rot_z(rotang)
  1001.             def convert(p,org=p1,rotmat=rotmat,fromAxisBase=fromAxisBase):
  1002.                 p = rotmat*p
  1003.                 return  fromAxisBase(p,org)
  1004.             p2, p3 = convert(p2), convert(p3)
  1005.             newface=self.face.copy()
  1006.             newface.setthreepoints((p1,p2,p3),2)
  1007.             undo = quarkx.action()
  1008.             undo.exchange(self.face, newface)
  1009.             editor.ok(undo, "Align Texture")
  1010.  
  1011.         alignitem = qmenu.item('Align to tagged edge',alignClick)
  1012.         if edge is None:
  1013.             alignitem.state=qmenu.disabled
  1014.  
  1015.         def action(dlgself, self=self, tagged=tagged, editor=editor, toFace=toFace):
  1016.             prop=dlgself.prop            
  1017.             tagged=toFace(tagged)
  1018.             p1, p2, p3, p4 = self.tp4
  1019.             newface = self.face.copy()
  1020.             diff=prop*(tagged-p1)
  1021.             if self.n==1:
  1022.                 newface.setthreepoints((p1, p1+diff, p3),2)
  1023.             elif self.n==2:
  1024.                 newface.setthreepoints((p1,p2,p1+diff),2)
  1025.             undo=quarkx.action()
  1026.             undo.exchange(self.face,newface)
  1027.             editor.ok(undo,'Proportional Glue to Tagged')
  1028.  
  1029.         def propGlueClick(m, action=action):
  1030.             PropGlueDlg(quarkx.clickform,action)
  1031.         
  1032.         propglueitem = qmenu.item('Proportional glue',propGlueClick)
  1033.         propglueitem.state=qmenu.disabled
  1034.         if tagged is not None and (self.n==1 or self.n==2):
  1035.             propglueitem.state=qmenu.normal
  1036.  
  1037.         return [glueitem, propglueitem, alignitem]
  1038.  
  1039.  
  1040.     def drag(self, v1, v2, flags, view):
  1041.         view.invalidate(1)
  1042.         self.dynp4 = None
  1043.         delta = v2-v1
  1044.  
  1045.         # force into the face plane
  1046.         normal = self.face.normal
  1047.         if not (flags&MB_CTRL):
  1048.             delta = qhandles.aligntogrid(delta, 0)
  1049.         delta = delta - normal*(normal*delta)   # back into the plane
  1050.  
  1051.         if not delta:
  1052.             return None, None
  1053.  
  1054.         p1,p2,p3,p4 = self.tp4
  1055.         p2 = p2 - p1
  1056.         p3 = p3 - p1
  1057.         if self.n==0:
  1058.             p1 = p1 + delta
  1059.             if flags&MB_CTRL:
  1060.                 p1 = qhandles.aligntogrid(p1, 1)
  1061.                 p1 = p1 - normal*(normal*p1-self.face.dist)   # back into the plane
  1062.             self.draghint = vtohint(p1-self.tp4[0])
  1063.         elif self.n==1:
  1064.             p2 = p2 + delta
  1065.             if flags&MB_CTRL:
  1066.                 p2 = qhandles.aligntogrid(p2, 1)
  1067.                 p2 = p2 - normal*(normal*p2)   # back into the plane
  1068.             self.draghint = vtohint(p2-self.tp4[1])
  1069.         elif self.n==2:
  1070.             p3 = p3 + delta
  1071.             if flags&MB_CTRL:
  1072.                 p3 = qhandles.aligntogrid(p3, 1)
  1073.                 p3 = p3 - normal*(normal*p3)   # back into the plane
  1074.             self.draghint = vtohint(p3-self.tp4[2])
  1075.         else:   # n==3:
  1076.             # ---- texture rotation begin ----
  1077.             if not normal:
  1078.                 return None, None
  1079.             texp4 = p2+p3
  1080.             m = qhandles.UserRotationMatrix(normal, texp4+qhandles.aligntogrid(delta, 0), texp4, flags&MB_CTRL)
  1081.             if m is None:
  1082.                 return None, None
  1083.             p2 = m*p2
  1084.             p3 = m*p3
  1085.             self.draghint = "%d degrees" % (math.acos(m[0,0])*180.0/math.pi)
  1086.             # ---- texture rotation end ----
  1087.  
  1088. #DECKER - 2001.08.13 - The 'if abs(p2^p3) < l*l*0.1:' makes it impossible to have a _huge_ first texture-axis and a _small_ second texture-axis
  1089. #        l = max((abs(p2), abs(p3)))
  1090. #        if abs(p2^p3) < l*l*0.1:
  1091. #            return None, None    # degenerate
  1092.  
  1093.         try:
  1094.             # Calculate the "angle" between the two texture-axes
  1095.             v = p2.normalized - p3.normalized
  1096.             len = abs(v)
  1097.             # Make sure that the "angle" isn't near "360-degrees" nor "0-degrees", else the texture would look rather weird!
  1098.             if len < 0.01 or len > 1.999:
  1099.                 return None, None    # degenerate
  1100.         except:
  1101.             return None, None    # math error
  1102. #/DECKER - 2001.08.13
  1103.  
  1104.         self.dynp4 = (p1,p1+p2,p1+p3,p1+p2+p3)
  1105.         r = self.face.copy()
  1106.         r.setthreepoints((p1,p1+p2,p1+p3), 2, self.texsrc)
  1107.         return [self.face], [r]
  1108.  
  1109.     def getdrawmap(self):
  1110.         return self.face, qhandles.refreshtimertex
  1111.  
  1112.  
  1113. class CyanLHandle0(CyanLHandle):
  1114.     "Texture moving of faces : cyan L base."
  1115.  
  1116.     def __init__(self, tp4, face, texsrc, handles):
  1117.         CyanLHandle.__init__(self, 0, tp4, face, texsrc)
  1118.         self.friends = handles
  1119.  
  1120.  
  1121.     def draw(self, view, cv, draghandle=None):
  1122.         dyn = (draghandle is self) or (draghandle in self.friends)
  1123.         if dyn:
  1124.             pencolor = RED
  1125.             tp4 = draghandle.dynp4
  1126.         else:
  1127.             tp4 = None
  1128.         if tp4 is None:
  1129.             pencolor = 0xF0CAA6
  1130.             tp4 = self.tp4
  1131.         pt = map(view.proj, tp4)
  1132.  
  1133.         # draw a grid while dragging
  1134.         if dyn:
  1135.             view.drawgrid(pt[1]-pt[0], pt[2]-pt[0], MAROON, DG_LINES, 0, tp4[0])
  1136.  
  1137.         # draw the cyan L
  1138.         cv.reset()
  1139.         cv.pencolor = BLACK
  1140.         cv.penwidth = 5
  1141.         cv.line(pt[0], pt[2])
  1142.         cv.line(pt[3], pt[3])
  1143.         cv.pencolor = pencolor
  1144.         cv.penwidth = 3
  1145.         cv.line(pt[0], pt[2])
  1146.         cv.line(pt[3], pt[3])
  1147.         cv.pencolor = BLACK
  1148.         cv.penwidth = 5
  1149.         cv.line(pt[0], pt[1])
  1150.         cv.pencolor = pencolor
  1151.         cv.penwidth = 3
  1152.         cv.line(pt[0], pt[1])
  1153.  
  1154. #
  1155. # A version of LinHandlesManager for Bezier texture.
  1156. #
  1157.  
  1158. class BTLinHandlesManager(qhandles.LinHandlesManager):
  1159.     "Linear Box manager for Bezier texture."
  1160.  
  1161.     def t2p(self, p):
  1162.         w,h = self.scale
  1163.         return quarkx.vect(p.x*w, p.y*h, 0.0)
  1164.  
  1165.     def p2t(self, p):
  1166.         w,h = self.scale
  1167.         return quarkx.vect(p.x/w, p.y/h, 0.0)
  1168.  
  1169.     def linear(self, sender, obj, center, matrix):
  1170.         if obj.type==":b3":
  1171.             obj.vst = sender.dynst = map(self.p2t, map(lambda v,center=center,matrix=matrix: matrix*(v-center)+center, map(self.t2p, obj.vst)))
  1172.         else:
  1173.             def maprow(p,center=center,matrix=matrix,self=self):
  1174.                 t = self.t2p(quarkx.vect(p.s, p.t, 0.0))
  1175.                 t = self.p2t(matrix*(t-center)+center)
  1176.                 return quarkx.vect(p.xyz + (t.x, t.y))
  1177.             tcp = []
  1178.             for row in obj.cp:
  1179.                 tcp.append(map(maprow, row))
  1180.             obj.cp = sender.dynst = tcp
  1181.  
  1182.     def translate(self, sender, obj, delta, forcetogrid):
  1183.         if obj.type==":b3":
  1184.             obj.vst = sender.dynst = map(self.p2t, map(lambda v,delta=delta: v+delta, map(self.t2p, obj.vst)))
  1185.         else:
  1186.             def maprow(p, delta=delta, self=self):
  1187.                 t = self.t2p(quarkx.vect(p.s, p.t, 0.0))
  1188.                 t = self.p2t(t+delta)
  1189.                 return quarkx.vect(p.xyz + (t.x, t.y))
  1190.             tcp = []
  1191.             for row in obj.cp:
  1192.                 tcp.append(map(maprow, row))
  1193.             obj.cp = sender.dynst = tcp
  1194.  
  1195.  
  1196. class CyanBezier2Handle(qhandles.GenericHandle):
  1197.     "Texture moving of BΘzier patches : cyan L vertices."
  1198.  
  1199.     undomsg = Strings[628]
  1200.     hint = "the shape is the fraction of the texture to map to the Bezier patch"
  1201.  
  1202.     def __init__(self, (i,j), cp, b2, scale):
  1203.         self.scale = scale
  1204.         qhandles.GenericHandle.__init__(self, self.t2p(cp[i][j]))
  1205.         self.b2 = b2
  1206.         self.ij = (i,j)
  1207.         self.hint = "Control Point (%d, %d)"%(i,j)
  1208.         self.cp = cp
  1209.         self.colormask = WHITE
  1210.         self.color = WHITE
  1211.  
  1212.     def t2p(self, p):
  1213.         w,h = self.scale
  1214.         return quarkx.vect(p.s*w, p.t*h, 0.0)
  1215.  
  1216.     def p2t(self, p, q):
  1217.         w,h = self.scale
  1218.         return quarkx.vect(q.xyz +(p.x/w, p.y/h))
  1219.  
  1220.     def drag(self, v1, v2, flags, view):
  1221.         view.invalidate(1)
  1222.         self.dynst = None
  1223.         delta = v2-v1
  1224.         if not (flags&MB_CTRL):
  1225.             delta = qhandles.aligntogrid(delta, 0)
  1226.  
  1227.         if flags&MB_CTRL:
  1228.             delta = qhandles.aligntogrid(self.pos + delta, 1) - self.pos
  1229.         if delta or (flags&MB_REDIMAGE):
  1230.             new = self.b2.copy()
  1231.             cp = map(list, self.b2.cp)
  1232.             i, j = self.ij
  1233.             w,h = self.scale
  1234.             td = quarkx.vect(0,0,0,delta.x/w,delta.y/h)
  1235.             moverow = (quarkx.keydown('\022')==1)  # ALT
  1236.             movecol = (quarkx.keydown('\020')==1)  # SHIFT
  1237.             from mapbezier import pointsToMove
  1238.             indexes = pointsToMove(moverow, movecol, i, j, len(cp), len(cp[0]))
  1239.             for m,n in indexes:
  1240.                 cp[m][n] = cp[m][n]+td
  1241.                 new.cp =cp
  1242.  
  1243.  
  1244.         if new is not None:
  1245.             self.dynst = new.cp
  1246.         return [self.b2], [new]
  1247.  
  1248.     def getdrawmap(self):
  1249.         return self.b2, qhandles.refreshtimertex
  1250.  
  1251.     def getcenter(self):
  1252.         cp = self.cp
  1253.         m, n = len(cp)-1, len(cp[0])-1
  1254.         return self.t2p(0.25*(cp[0][0]+cp[m][0]+cp[0][n]+cp[m][n]))
  1255.  
  1256.  
  1257.  
  1258. class CyanBezier2Handle0(CyanBezier2Handle):
  1259.     "Texture moving of BΘzier patches : cyan L base."
  1260.  
  1261.     def __init__(self, cp, b2, handles, scale):
  1262.         CyanBezier2Handle.__init__(self, (0,0), cp, b2, scale)
  1263.         self.friends = handles
  1264.  
  1265.     def draw(self, view, cv, draghandle=None):
  1266.         dyn = (draghandle is self) or (draghandle in self.friends)
  1267.         if dyn:
  1268.             pencolor = RED
  1269.             try:
  1270.                 cp = draghandle.dynst
  1271.             except (AttributeError):
  1272.                 cp = None
  1273.         else:
  1274.             cp = None
  1275.         if cp is None:
  1276.             pencolor = 0xF0CAA6
  1277.             cp = self.cp
  1278.         pt = []
  1279.         for row in cp:
  1280.             pt.append(map(view.proj, map(self.t2p, row)))
  1281.         m, n = len(cp), len(cp[0])
  1282.         # draw the cyan shape
  1283.         cv.reset()
  1284.         def line(frm, to, bold, cv=cv, pencolor=pencolor):
  1285.             cv.pencolor = BLACK
  1286.             cv.penwidth = (3,5)[bold]
  1287.             cv.line(frm, to)
  1288.             cv.pencolor = pencolor
  1289.             cv.penwidth = (1,3)[bold]
  1290.             cv.line(frm, to)
  1291.         # vertical thin lines
  1292.         for i in range(m-1):
  1293.             for j in range(1, n-1):
  1294.                 line(pt[i][j], pt[i+1][j], 0)
  1295.         # horizontal thin lines
  1296.         for i in range(1,m-1):
  1297.             for j in range(n-1):
  1298.                 line(pt[i][j], pt[i][j+1], 0)
  1299.         # vertical bold lines
  1300.         for i in range(m-1):
  1301.             for j in (0,n-1):
  1302.                 line(pt[i][j], pt[i+1][j], 1)
  1303.         # horizontal bold lines
  1304.         for i in (0,m-1):
  1305.             for j in range(n-1):
  1306.                 line(pt[i][j], pt[i][j+1], 1)
  1307.  
  1308.  
  1309. class EyePositionMap(qhandles.EyePosition):
  1310.       pass
  1311.  
  1312. #
  1313. # Functions to build common lists of handles.
  1314. #
  1315.  
  1316.  
  1317. def BuildHandles(editor, ex, view):
  1318.     "Build a list of handles to display on the map views."
  1319.  
  1320.     fs = ex.uniquesel
  1321.     if (fs is None) or editor.linearbox:
  1322.         #
  1323.         # Display a linear mapping box.
  1324.         #
  1325.         list = ex.sellist
  1326.         box = quarkx.boundingboxof(list)
  1327.         if box is None:
  1328.             h = []
  1329.         else:
  1330.             manager = qhandles.LinHandlesManager(MapColor("Linear"), box, list)
  1331.             h = manager.BuildHandles(editor.interestingpoint())
  1332.     else:
  1333.         #
  1334.         # Get the list of handles from the entity manager.
  1335.         #
  1336.         h = mapentities.CallManager("handles", fs, editor, view)
  1337.     #
  1338.     # Add the 3D view "eyes".
  1339.     #
  1340.     for v in editor.layout.views:
  1341.         if (v is not view) and (v.info["type"] == "3D"):
  1342.             h.append(EyePositionMap(view, v))
  1343.             h.append(MapEyeDirection(view, v))
  1344.     return qhandles.FilterHandles(h, SS_MAP)
  1345.  
  1346.  
  1347. def BuildCyanLHandles(editor, face):
  1348.     "Build a list of handles to display a cyan L over a face' texture."
  1349.  
  1350.     tp = face.threepoints(2, editor.TexSource)
  1351.     if tp is None:
  1352.         return []
  1353.     tp4 = tp + (tp[1]+tp[2]-tp[0],)
  1354.     handles = [CyanLHandle(1,tp4,face,editor.TexSource), CyanLHandle(2,tp4,face,editor.TexSource), CyanLHandle(3,tp4,face,editor.TexSource)]
  1355.     return qhandles.FilterHandles([CyanLHandle0(tp4, face, editor.TexSource, handles)] + handles, SS_MAP)
  1356.  
  1357.  
  1358.  
  1359. #
  1360. # Drag Objects
  1361. #
  1362.  
  1363. class RectSelDragObject(qhandles.RectangleDragObject):
  1364.     "A red rectangle that selects the polyhedrons it touches."
  1365.  
  1366.     Hint = hintPlusInfobaselink("Rectangular selection of POLYHEDRONS||Rectangular selection of POLYHEDRONS:\n\nAfter you click on this button, click and move the mouse on the map to draw a rectangle; all polyhedrons touching this rectangle will be selected.\n\nHold down Ctrl to prevent already selected polyhedron from being unselected first.", "intro.mapeditor.toolpalettes.mousemodes.html#selectpoly")
  1367.  
  1368.     def rectanglesel(self, editor, x,y, rectangle):
  1369.         if not ("T" in self.todo):
  1370.             editor.layout.explorer.uniquesel = None
  1371.         polylist = FindSelectable(editor.Root, ":p")
  1372.         lastsel = None
  1373.         for p in polylist:
  1374.             if rectangle.intersects(p):
  1375.                 p.selected = 1
  1376.                 lastsel = p
  1377.         if lastsel is not None:
  1378.             editor.layout.explorer.focus = lastsel
  1379.             editor.layout.explorer.selchanged()
  1380.  
  1381.  
  1382. #
  1383. # Mouse Clicking and Dragging on map views.
  1384. #
  1385.  
  1386. def MouseDragging(self, view, x, y, s, handle):
  1387.     "Mouse Drag on a Map View."
  1388.  
  1389.     #
  1390.     # qhandles.MouseDragging builds the DragObject.
  1391.     #
  1392.     if handle is not None:
  1393.         s = handle.click(self)
  1394.         if s and ("S" in s):
  1395.             self.layout.actionmpp()  # update the multi-pages-panel
  1396.  
  1397.     return qhandles.MouseDragging(self, view, x, y, s, handle, MapColor("GrayImage"))
  1398.  
  1399.  
  1400. def ClickOnView(editor, view, x, y):
  1401.     #
  1402.     # defined in QkPyMapview.pas
  1403.     #
  1404.     return view.clicktarget(editor.Root, x, y)
  1405.  
  1406.  
  1407. def MapAuxKey(keytag):
  1408.     return quarkx.setupsubset(SS_GENERAL,"AuxKeys")[keytag]
  1409.  
  1410. def wantPoly():
  1411.     return quarkx.keydown(MapAuxKey('Select Brushes'))==1
  1412.  
  1413. def wantFace():
  1414.     return quarkx.keydown(MapAuxKey('Select Faces'))==1
  1415.     
  1416. def wantCurve():
  1417.      return quarkx.keydown(MapAuxKey('Select Curves'))==1    
  1418.  
  1419. def wantEntity():
  1420.     return quarkx.keydown(MapAuxKey('Select Entities'))==1
  1421.  
  1422. def MouseClicked(self, view, x, y, s, handle):
  1423.     "Mouse Click on a Map view."
  1424.  
  1425.     #
  1426.     # qhandles.MouseClicked manages the click but doesn't actually select anything
  1427.     #
  1428.  
  1429.     flags = qhandles.MouseClicked(self, view, x, y, s, handle)
  1430. #    debug('flagz: '+s)
  1431.     if view.info["type"]=="3D":
  1432.         self.last3DView = view
  1433.     if "1" in flags:
  1434.         #
  1435.         # This mouse click must select something.
  1436.         #
  1437.  
  1438.         self.layout.setupdepth(view)
  1439.         choice = ClickOnView(self, view, x, y)
  1440.          # this is the list of polys & entities we clicked on
  1441.          # the members of the choice list are pairs, for polys,
  1442.          #   the first is the poly, the second the face
  1443.          #
  1444.         if wantCurve():
  1445.             choice=filter(lambda obj:obj[1].type==':b2', choice)
  1446.         elif wantFace() or wantPoly():
  1447.             choice=filter(lambda obj:obj[1].type==':p', choice)
  1448.         elif wantEntity():
  1449.  
  1450.             def getentities(choice):
  1451.                 result=[]
  1452.                 for item in choice:
  1453.                     if item[1].type==':e':
  1454.                         result.append(item)
  1455.                     else:
  1456.                         parent=item[1].treeparent   
  1457.                         while parent.name!='worldspawn:b':
  1458.                             if parent.type==':b':    # eventually it will hit worldspawn
  1459.                                 result.append((choice[0], parent, parent))
  1460.                                 break
  1461.                             parent=parent.treeparent
  1462.                 return result
  1463.                 
  1464.             choice=getentities(choice)
  1465.         if len(choice):
  1466.             wantpoly = wantPoly()
  1467.             wantface = wantFace()
  1468.             choice.sort()   # list of (clickpoint,object) tuples - sort by depth
  1469.             last = qhandles.findlastsel(choice)
  1470.             #
  1471.             # Note: dropping 'and last' from below seems to deliver
  1472.             #   background menu on RMB when nothing is selected, seems
  1473.             #   better
  1474.             #
  1475.             if ("M" in s):    # if Menu, we try to keep the currently selected objects
  1476.                 return flags
  1477.             if "T" in s:    # if Multiple selection request
  1478.                 #
  1479.                 # if selection is frozen, ignore request
  1480.                 #
  1481.                 if getAttr(self, 'frozenselection') is not None:
  1482.                     return flags
  1483.                 obj = qhandles.findnextobject(choice)
  1484.                 obj.togglesel()
  1485.                 if obj.selected:
  1486.                     self.layout.explorer.focus = obj
  1487.                 self.layout.explorer.selchanged()
  1488.             else:
  1489.                 if wantface or wantpoly:
  1490.                     keep=1
  1491.                 else:
  1492.                     keep=0
  1493.                 last = qhandles.findlastsel(choice,keep)
  1494.                 if last:  last = last - len(choice)
  1495.                 #
  1496.                 #  if the selection is 'frozen', it can only be changed
  1497.                 #    by another SHIFT-selection, or the ESC key
  1498.                 #
  1499.                 # if the selection if frozen and this isn't a change
  1500.                 #    frozen selection request, ignore it
  1501.                 #
  1502.                 if not "F" in s:
  1503.                     if getAttr(self,'frozenselection') is not None:
  1504.                        return flags
  1505.                 if "F" in s:
  1506.                     self.frozenselection = 1
  1507.                 if wantface:
  1508.                     self.layout.explorer.uniquesel = choice[last][2]
  1509.                 else:
  1510.                     self.layout.explorer.uniquesel = choice[last][1]
  1511.  
  1512.         else:
  1513.             if not ("T" in s):    # clear current selection
  1514.                 #
  1515.                 # if the selection is 'frozen', we don't want to be
  1516.                 #   able to clear it with a mouseclick, only ESC
  1517.                 #
  1518.                 if getAttr(self,'frozenselection') is not None:
  1519.                       pass
  1520.                 else:
  1521.                    self.layout.explorer.uniquesel = None
  1522.         return flags+"S"
  1523.     return flags
  1524.  
  1525.  
  1526. #
  1527. # Single face map view display for the Multi-Pages Panel.
  1528. #
  1529.  
  1530. def viewsingleface(editor, view, face):
  1531.     "Special code to view a single face with handles to move the texture."
  1532.  
  1533.     def drawsingleface(view, face=face, editor=editor):
  1534.         view.drawmap(face)   # textured face
  1535.         view.solidimage(editor.TexSource)
  1536.         #for poly in face.faceof:
  1537.         #    view.drawmap(poly, DM_OTHERCOLOR, 0x2584C9)   # draw the full poly contour
  1538.         view.drawmap(face, DM_REDRAWFACES|DM_OTHERCOLOR, 0x2584C9)   # draw the face contour
  1539.         editor.finishdrawing(view)
  1540.         # end of drawsingleface
  1541.  
  1542.     origin = face.origin
  1543.     if origin is None: return
  1544.     n = face.normal
  1545.     if not n: return
  1546.  
  1547.     h = []
  1548.      # add the vertices of the face
  1549.     for p in face.faceof:
  1550.         if p.type == ':p':
  1551.             for v in face.verticesof(p):
  1552.                 h.append(VertexHandle(v, p))
  1553.     view.handles = qhandles.FilterHandles(h, SS_MAP) + BuildCyanLHandles(editor, face)
  1554.  
  1555. #DECKER - begin
  1556.     #FIXME - Put a check for an option-switch here, so people can choose which they want (fixed-zoom/scroll, or reseting-zoom/scroll)
  1557.     oldx, oldy, doautozoom = 0, 0, 0
  1558.     try:
  1559.         oldorigin = view.info["origin"]
  1560.         if not abs(origin - oldorigin):
  1561.             oldscale = view.info["scale"]
  1562.             if oldscale is None:
  1563.                 doautozoom = 1
  1564.             oldx, oldy = view.scrollbars[0][0], view.scrollbars[1][0]
  1565.         else:
  1566.             doautozoom = 1
  1567.     except:
  1568.         doautozoom = 1
  1569.  
  1570.     if doautozoom:
  1571.         oldscale = 0.01
  1572. #DECKER - end
  1573.  
  1574.     v = orthogonalvect(n, editor.layout.views[0])
  1575.     view.flags = view.flags &~ (MV_HSCROLLBAR | MV_VSCROLLBAR)
  1576.     view.viewmode = "tex"
  1577.     view.info = {"type": "2D",
  1578.                  "matrix": ~ quarkx.matrix(v, v^n, -n),
  1579.                  "bbox": quarkx.boundingboxof([face] + map(lambda h: h.pos, view.handles)),
  1580.                  "scale": oldscale, #DECKER
  1581.                  "custom": singlefacezoom,
  1582.                  "origin": origin,
  1583.                  "noclick": None,
  1584.                  "mousemode": None }
  1585.     singlefacezoom(view, origin)
  1586.     if doautozoom: #DECKER
  1587.         singlefaceautozoom(view, face) #DECKER
  1588.     editor.setupview(view, drawsingleface, 0)
  1589.     if (oldx or oldy) and not doautozoom: #DECKER
  1590.         view.scrollto(oldx, oldy) #DECKER
  1591.     return 1
  1592.  
  1593.  
  1594. def singlefaceautozoom(view, face):
  1595.     scale1, center1 = AutoZoom([view], view.info["bbox"], margin=(36,34))
  1596.     if scale1 is None:
  1597.         return 0
  1598.     if scale1>1.0:
  1599.         scale1=1.0
  1600.     if abs(scale1-view.info["scale"])<=epsilon:
  1601.         return 0
  1602.     view.info["scale"] = scale1
  1603.     singlefacezoom(view, center1)
  1604.     return 1
  1605.  
  1606.     #for test in (0,1):
  1607.     #    scale1, center1 = AutoZoom([view], view.info["bbox"], margin=(36,34))
  1608.     #    if (scale1 is None) or (scale1>=1.0) or (abs(scale1-view.info["scale"])<=epsilon):
  1609.     #        return test
  1610.     #    view.info["scale"] = scale1
  1611.     #    singlefacezoom(view, center1)   # do it twice because scroll bars may disappear
  1612.     #return 1
  1613.  
  1614.  
  1615. def singlefacezoom(view, center=None):
  1616.     if center is None:
  1617.         center = view.screencenter
  1618.     view.setprojmode("2D", view.info["matrix"]*view.info["scale"], 0)
  1619.     bmin, bmax = view.info["bbox"]
  1620.     x1=y1=x2=y2=None
  1621.     for x in (bmin.x,bmax.x):   # all 8 corners of the bounding box
  1622.         for y in (bmin.y,bmax.y):
  1623.             for z in (bmin.z,bmax.z):
  1624.                 p = view.proj(x,y,z)
  1625.                 if (x1 is None) or (p.x<x1): x1=p.x
  1626.                 if (y1 is None) or (p.y<y1): y1=p.y
  1627.                 if (x2 is None) or (p.x>x2): x2=p.x
  1628.                 if (y2 is None) or (p.y>y2): y2=p.y
  1629.     view.setrange(x2-x1+36, y2-y1+34, 0.5*(bmin+bmax))
  1630.  
  1631.      # trick : if we are far enough and scroll bars are hidden,
  1632.      # the code below clamb the position of "center" so that
  1633.      # the picture is completely inside the view.
  1634.     x1=y1=x2=y2=None
  1635.     for x in (bmin.x,bmax.x):   # all 8 corners of the bounding box
  1636.         for y in (bmin.y,bmax.y):
  1637.             for z in (bmin.z,bmax.z):
  1638.                 p = view.proj(x,y,z)    # re-proj... because of setrange
  1639.                 if (x1 is None) or (p.x<x1): x1=p.x
  1640.                 if (y1 is None) or (p.y<y1): y1=p.y
  1641.                 if (x2 is None) or (p.x>x2): x2=p.x
  1642.                 if (y2 is None) or (p.y>y2): y2=p.y
  1643.     w,h = view.clientarea
  1644.     w,h = (w-36)/2, (h-34)/2
  1645.     x,y,z = view.proj(center).tuple
  1646.     t1,t2 = x2-w,x1+w
  1647.     if t2>=t1:
  1648.         if x<t1: x=t1
  1649.         elif x>t2: x=t2
  1650.     t1,t2 = y2-h,y1+h
  1651.     if t2>=t1:
  1652.         if y<t1: y=t1
  1653.         elif y>t2: y=t2
  1654.     view.screencenter = view.space(x,y,z)
  1655.     p = view.proj(view.info["origin"])
  1656.     view.depth = (p.z-0.1, p.z+100.0)
  1657.  
  1658. #
  1659. # Single bezier map view display for the Multi-Pages Panel.
  1660. #
  1661.  
  1662. def viewsinglebezier(view, layout, patch):
  1663.     cpts = patch.cp
  1664.     if cpts is None:
  1665.         return
  1666.     texlist = quarkx.texturesof([patch])
  1667.     ed = mapeditor()
  1668.     if len(texlist)==1:
  1669. #        tex = quarkx.loadtexture(texlist[0], layout.editor.TexSource)
  1670.         tex = quarkx.loadtexture(texlist[0], ed.TexSource)
  1671.         try:
  1672.             tex = tex.disktexture
  1673.             w,h = tex["Size"]
  1674.         except:
  1675.             pass
  1676.         else:
  1677.             if type==':b3':
  1678.                 vst = patch.vst
  1679.             else:
  1680.                 vst = []
  1681.                 for row in patch.cp:
  1682.                     for p in row:
  1683.                         vst.append(quarkx.vect(p.s, p.t, 0))
  1684.             matrix = quarkx.matrix((1,0,0), (0,1,0), (0,0,1))
  1685.             xmin = xmax = vst[0].x
  1686.             ymin = ymax = vst[0].y
  1687.             for v in vst[1:]:
  1688.                 if v.x<xmin: xmin=v.x
  1689.                 if v.x>xmax: xmax=v.x
  1690.                 if v.y<ymin: ymin=v.y
  1691.                 if v.y>ymax: ymax=v.y
  1692.             destx, desty = view.clientarea
  1693.             scale = (destx-35) / ((xmax-xmin+0.05)*w)
  1694.             scaley = (desty-35) / ((ymax-ymin+0.05)*h)
  1695.             if scaley<scale: scale=scaley
  1696.             if scale<0.01: scale=0.01
  1697.             if scale>1.0: scale=1.0
  1698.  
  1699.             view.setprojmode("2D", matrix*scale)
  1700.             view.info = {"type": "2D",
  1701.                    "matrix": matrix,
  1702.                    "scale": scale,
  1703.                    "custom": singlebezierzoom,
  1704.                    "noclick": None,
  1705.                    "mousemode": None }
  1706.             def draw1(view, finish=layout.editor.finishdrawing, w=w, h=h):
  1707.                 pt = view.space(quarkx.vect(0,0,0))
  1708.                 pt = view.proj(quarkx.vect(math.floor(pt.x), math.floor(pt.y), 0))
  1709.                 #view.canvas().painttexture(tex, (pt.x,pt.y)+view.clientarea, 0)
  1710.                 view.drawgrid(quarkx.vect(w*view.info["scale"],0,0), quarkx.vect(0,h*view.info["scale"],0), MAROON, DG_LINES, 0, quarkx.vect(0,0,0))
  1711.                 finish(view)
  1712.             view.ondraw = draw1
  1713.             view.onmouse = layout.editor.mousemap
  1714.             cp = patch.cp
  1715.             h2 = []
  1716.             m, n = len(cp), len(cp[0])
  1717.             for i in range(m):
  1718.                 for j in range(n):
  1719.                     h2.append(CyanBezier2Handle((i,j), cp, patch, (w, h)))
  1720.             mainhandle = CyanBezier2Handle0(cp, patch, h2, (w,h))
  1721.             h2 = [mainhandle] + h2
  1722.  
  1723.  
  1724.             #
  1725.             # Display a linear mapping box.
  1726.             #
  1727.             manager = BTLinHandlesManager(MapColor("Linear"),
  1728.                   (quarkx.vect(w*xmin,h*ymin,0),quarkx.vect(w*xmax,h*ymax,0)), [patch])
  1729.             manager.scale = w,h
  1730. #
  1731. # Linear mapping in bez box, can't make it work
  1732. #
  1733. #            if layout.editor.linearbox:
  1734. #                minimal = None
  1735. #            else:
  1736. #                minimal = (view, layout.editor.gridstep or 32)
  1737. #            h1 = manager.BuildHandles(minimal=minimal)
  1738. #            getdrawmap1 = lambda patch=patch: (patch, qhandles.refreshtimertex)
  1739. #            for i in h1:
  1740. #                i.getdrawmap = getdrawmap1
  1741. #            mainhandle.friends = mainhandle.friends + h1
  1742. #            view.handles = h2 + h1
  1743.             view.handles = h2
  1744.             view.background = tex, quarkx.vect(0,0,0), 1.0
  1745.             view.screencenter = mainhandle.getcenter()
  1746.             return 1
  1747.  
  1748. def singlebezierzoom(view):
  1749.     sc = view.screencenter
  1750.     view.setprojmode("2D", view.info["matrix"]*view.info["scale"], 0)
  1751.     view.screencenter = sc
  1752.  
  1753. def GetUserCenter(obj):
  1754. #    debug('type: '+`type(obj)`)
  1755.     if type(obj) is type([]):  # obj is list
  1756.         if len(obj)==1 and obj[0]["usercenter"] is not None:
  1757.             uc = obj[0]["usercenter"]
  1758.         else:
  1759.             try:
  1760.                 box=quarkx.boundingboxof(obj)
  1761.                 return (box[0]+box[1])/2
  1762.             except:
  1763.                 return quarkx.vect(0,0,0)
  1764.     else:
  1765.         uc = obj["usercenter"]
  1766.     if uc is None:
  1767.         uc = mapentities.ObjectOrigin(obj).tuple
  1768. #        debug(' oo '+`uc`)
  1769.     return quarkx.vect(uc)
  1770.  
  1771. def SetUserCenter(obj, v):
  1772.     obj["usercenter"] = v.tuple
  1773.  
  1774. def macro_usercenter(self):
  1775.     from qeditor import mapeditor
  1776.     editor=mapeditor()
  1777.     if editor is None: return
  1778.     dup = editor.layout.explorer.uniquesel
  1779.     if dup is None: return
  1780.     undo = quarkx.action()
  1781.     from mapentities import ObjectOrigin
  1782.     tup = ObjectOrigin(dup).tuple
  1783.     undo.setspec(dup,'usercenter',tup)
  1784.     editor.ok(undo,'add usercenter')
  1785.     editor.invalidateviews()
  1786.  
  1787. qmacro.MACRO_usercenter = macro_usercenter
  1788.  
  1789. class UserCenterHandle(CenterHandle):
  1790.  
  1791.     hint = "Usercenter handle (Ctrl key: force to grid)||The usercenter is used to control the pivot-point for rotations and symmetry operations."
  1792.  
  1793.     def __init__(self, dup):
  1794.         pos = GetUserCenter(dup)
  1795.         CenterHandle.__init__(self, pos, dup, MapColor("Axis"))
  1796.  
  1797.     def drag(self, v1, v2, flags, view):
  1798.         if flags&MB_CTRL:
  1799.             v2 = qhandles.aligntogrid(v2, 1)
  1800.         delta = v2-v1
  1801.         dup = self.centerof.copy()
  1802.         SetUserCenter(dup, GetUserCenter(dup)+delta)
  1803.         return [self.centerof], [dup]
  1804.  
  1805.  
  1806. # ----------- REVISION HISTORY ------------
  1807. #
  1808. #$Log: maphandles.py,v $
  1809. #Revision 1.37  2003/03/15 20:54:20  cdunde
  1810. #To update hints and add infobase links
  1811. #
  1812. #Revision 1.36  2003/03/06 22:21:34  tiglari
  1813. #change for Py2.x compatibility
  1814. #
  1815. #Revision 1.35  2002/05/18 22:30:42  tiglari
  1816. #remove debug statement
  1817. #
  1818. #Revision 1.34  2002/05/13 10:36:58  tiglari
  1819. #support frozen selections (don't change until another frozen selection is made,
  1820. #or they are cancelled with ESC or unfreeze selection)
  1821. #
  1822. #Revision 1.33  2001/09/26 22:37:24  tiglari
  1823. #change close button to cancel in proportional glue dialog
  1824. #
  1825. #Revision 1.32  2001/09/24 23:56:42  tiglari
  1826. #store cyanlhandle glue proportion in setup, fix some issues
  1827. #
  1828. #Revision 1.31  2001/09/24 10:15:19  tiglari
  1829. #proportional glue for CyanLHandles
  1830. #
  1831. #Revision 1.30  2001/08/16 20:09:15  decker_dk
  1832. #Support for snap-to-grid in UserCenterHandle drag.
  1833. #Specific hint for a UserCenterHandle.
  1834. #
  1835. #Revision 1.29  2001/08/15 17:52:42  decker_dk
  1836. #Exception-catch for def GetUserCenter(), in case "return (box[0]+box[1])/2" fails.
  1837. #
  1838. #Revision 1.28  2001/08/13 17:45:46  decker_dk
  1839. #Fixed problem where a '<huge> <small>' texture-scale caused the code to ignore further changes to the texture on the face.
  1840. #
  1841. #Revision 1.27  2001/07/27 11:35:49  tiglari
  1842. #revert code 4 setthreepoints to code 2
  1843. #
  1844. #Revision 1.26  2001/07/24 00:04:48  tiglari
  1845. #more comments for texture-L RMB menu items
  1846. #
  1847. #Revision 1.25  2001/06/14 12:17:36  tiglari
  1848. #note last 3d view clicked on in mouseclicked
  1849. #
  1850. #Revision 1.24  2001/06/13 20:58:44  tiglari
  1851. #Add map-specific EyePosition handle
  1852. #
  1853. #Revision 1.23  2001/05/12 10:12:22  tiglari
  1854. #fix usercenter button macro bug (add 'is None' ...)
  1855. #
  1856. #Revision 1.22  2001/05/06 06:03:38  tiglari
  1857. #add Edge Handle
  1858. #
  1859. #Revision 1.21  2001/04/26 22:45:03  tiglari
  1860. #face-only selection & texture L RMB
  1861. #
  1862. #Revision 1.20  2001/04/17 03:18:01  tiglari
  1863. #attempt to fix vertex-drag crash
  1864. #
  1865. #Revision 1.19  2001/04/10 08:54:42  tiglari
  1866. #remove debug statements from vertex dragging code
  1867. #
  1868. #Revision 1.18  2001/04/06 04:46:04  tiglari
  1869. #clean out some debug statements
  1870. #
  1871. #Revision 1.17  2001/04/05 12:36:13  tiglari
  1872. #revise vertex move code to try to reduce drift of non-drug vertexes
  1873. #
  1874. #Revision 1.13.2.3  2001/04/04 10:30:17  tiglari
  1875. #find more fixed points, clean up code
  1876. #
  1877. #Revision 1.13.2.2  2001/04/03 08:54:51  tiglari
  1878. #more vertex drag anti-drift.  Very convoluted,if it helps, it needs to be
  1879. # cleaned up.
  1880. #
  1881. #Revision 1.15  2001/04/02 21:09:44  tiglari
  1882. #fixes to getusercenter, ntp tuple-hood
  1883. #
  1884. #Revision 1.14  2001/04/02 09:23:11  tiglari
  1885. #stuck various things to try to nail down supposed fixpoint vertices in the
  1886. #vtx movement code
  1887. #
  1888. #Revision 1.13  2001/04/01 00:07:13  tiglari
  1889. #revisions to GetUserCenter
  1890. #
  1891. #Revision 1.12  2001/03/31 10:15:22  tiglari
  1892. #support for usercenter specific
  1893. #
  1894. #Revision 1.11  2001/03/01 19:14:58  decker_dk
  1895. #Fix for CyanBezier2Handle.drag 'if new:'. Now testing for 'if new is not None:'.
  1896. #
  1897. #Revision 1.10  2001/02/28 09:46:43  tiglari
  1898. #linear mapping handles removed from bez page
  1899. #
  1900. #Revision 1.9  2001/02/25 11:22:51  tiglari
  1901. #bezier page support, transplanted with permission from CryEd (CryTek)
  1902. #
  1903. #Revision 1.8  2001/02/07 18:40:47  aiv
  1904. #bezier texture vertice page started.
  1905. #
  1906. #Revision 1.7  2000/06/17 07:32:06  tiglari
  1907. #a slight change to clickedview
  1908. #
  1909. #Revision 1.6  2000/06/16 10:44:54  tiglari
  1910. #CenterHandle menu function adds clickedview to editor.layout
  1911. #(for support of perspective-driven curve creation in mb2curves.py)
  1912. #
  1913. #Revision 1.5  2000/06/02 16:00:22  alexander
  1914. #added cvs headers
  1915. #
  1916.